/*
// Licensed to Julian Hyde under one or more contributor license
// agreements. See the NOTICE file distributed with this work for
// additional information regarding copyright ownership.
//
// Julian Hyde licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except in
// compliance with the License. You may obtain a copy of the License at:
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
 */
package org.olap4j.mdx;

import java.util.AbstractList;
import java.util.Arrays;
import java.util.List;

/**
 * Region of parser source code.
 * 
 * <p>
 * The main purpose of a ParseRegion is to give detailed locations in error messages and warnings from the parsing and
 * validation process.
 * 
 * <p>
 * A region has a start and end line number and column number. A region is a point if the start and end positions are
 * the same.
 * 
 * <p>
 * The line and column number are one-based, because that is what end-users understand.
 * 
 * <p>
 * A region's end-points are inclusive. For example, in the code
 * 
 * <blockquote>
 * 
 * <pre>
 * SELECT FROM [Sales]
 * </pre>
 * 
 * </blockquote>
 * 
 * the <code>SELECT</code> token has region [1:1, 1:6].
 * 
 * <p>
 * Regions are immutable.
 * 
 * @author jhyde
 */
public class ParseRegion {
	private final int startLine;
	private final int startColumn;
	private final int endLine;
	private final int endColumn;

	/**
	 * Creates a ParseRegion.
	 * 
	 * <p>
	 * All lines and columns are 1-based and inclusive. For example, the token "select" in "select from [Sales]" has a
	 * region [1:1, 1:6].
	 * 
	 * @param startLine
	 *            Line of the beginning of the region
	 * @param startColumn
	 *            Column of the beginning of the region
	 * @param endLine
	 *            Line of the end of the region
	 * @param endColumn
	 *            Column of the end of the region
	 */
	public ParseRegion(int startLine, int startColumn, int endLine, int endColumn) {
		assert endLine >= startLine;
		assert endLine > startLine || endColumn >= startColumn;
		this.startLine = startLine;
		this.startColumn = startColumn;
		this.endLine = endLine;
		this.endColumn = endColumn;
	}

	/**
	 * Creates a ParseRegion.
	 * 
	 * All lines and columns are 1-based.
	 * 
	 * @param line
	 *            Line of the beginning and end of the region
	 * @param column
	 *            Column of the beginning and end of the region
	 */
	public ParseRegion(int line, int column) {
		this(line, column, line, column);
	}

	/**
	 * Return starting line number (1-based).
	 * 
	 * @return 1-based starting line number
	 */
	public int getStartLine() {
		return startLine;
	}

	/**
	 * Return starting column number (1-based).
	 * 
	 * @return 1-based starting column number
	 */
	public int getStartColumn() {
		return startColumn;
	}

	/**
	 * Return ending line number (1-based).
	 * 
	 * @return 1-based ending line number
	 */
	public int getEndLine() {
		return endLine;
	}

	/**
	 * Return ending column number (1-based).
	 * 
	 * @return 1-based starting endings column number
	 */
	public int getEndColumn() {
		return endColumn;
	}

	/**
	 * Returns a string representation of this ParseRegion.
	 * 
	 * <p>
	 * Regions are of the form <code>[startLine:startColumn, endLine:endColumn]</code>, or
	 * <code>[startLine:startColumn]</code> for point regions.
	 * 
	 * @return string representation of this ParseRegion
	 */
	public String toString() {
		return "[" + startLine + ":" + startColumn + ((isPoint()) ? "" : ", " + endLine + ":" + endColumn) + "]";
	}

	/**
	 * Returns whether this region has the same start and end point.
	 * 
	 * @return whether this region has the same start and end point
	 */
	public boolean isPoint() {
		return endLine == startLine && endColumn == startColumn;
	}

	public int hashCode() {
		return startLine ^ (startColumn << 2) ^ (endLine << 4) ^ (endColumn << 8);
	}

	public boolean equals(Object obj) {
		if (obj instanceof ParseRegion) {
			final ParseRegion that = (ParseRegion) obj;
			return this.startLine == that.startLine && this.startColumn == that.startColumn
					&& this.endLine == that.endLine && this.endColumn == that.endColumn;
		} else {
			return false;
		}
	}

	/**
	 * Combines this region with other regions.
	 * 
	 * @param nodes
	 *            Source code regions
	 * @return region which represents the span of the given regions
	 */
	public ParseRegion plus(final ParseTreeNode... nodes) {
		return plusAll(new AbstractList<ParseRegion>() {
			public ParseRegion get(int index) {
				final ParseTreeNode node = nodes[index];
				if (node == null) {
					return null;
				}
				return node.getRegion();
			}

			public int size() {
				return nodes.length;
			}
		});
	}

	public ParseRegion plus(final List<? extends ParseTreeNode> nodes) {
		if (nodes == null) {
			return this;
		}
		return plusAll(new AbstractList<ParseRegion>() {
			public ParseRegion get(int index) {
				final ParseTreeNode node = nodes.get(index);
				if (node == null) {
					return null;
				}
				return node.getRegion();
			}

			public int size() {
				return nodes.size();
			}
		});
	}

	/**
	 * Combines this region with other regions.
	 * 
	 * @param regions
	 *            Source code regions
	 * @return region which represents the span of the given regions
	 */
	public ParseRegion plus(ParseRegion... regions) {
		return plusAll(Arrays.asList(regions));
	}

	/**
	 * Combines this region with a list of parse tree nodes to create a region which spans from the first point in the
	 * first to the last point in the other.
	 * 
	 * @param regions
	 *            Collection of source code regions
	 * @return region which represents the span of the given regions
	 */
	public ParseRegion plusAll(Iterable<ParseRegion> regions) {
		return sum(regions, getStartLine(), getStartColumn(), getEndLine(), getEndColumn());
	}

	/**
	 * Combines the parser positions of a list of nodes to create a position which spans from the beginning of the first
	 * to the end of the last.
	 * 
	 * @param nodes
	 *            Collection of parse tree nodes
	 * @return region which represents the span of the given nodes
	 */
	public static ParseRegion sum(Iterable<ParseRegion> nodes) {
		return sum(nodes, Integer.MAX_VALUE, Integer.MAX_VALUE, -1, -1);
	}

	private static ParseRegion sum(Iterable<ParseRegion> regions, int startLine, int startColumn, int endLine,
			int endColumn) {
		int testLine;
		int testColumn;
		for (ParseRegion region : regions) {
			if (region == null) {
				continue;
			}
			testLine = region.getStartLine();
			testColumn = region.getStartColumn();
			if ((testLine < startLine) || ((testLine == startLine) && (testColumn < startColumn))) {
				startLine = testLine;
				startColumn = testColumn;
			}

			testLine = region.getEndLine();
			testColumn = region.getEndColumn();
			if ((testLine > endLine) || ((testLine == endLine) && (testColumn > endColumn))) {
				endLine = testLine;
				endColumn = testColumn;
			}
		}
		return new ParseRegion(startLine, startColumn, endLine, endColumn);
	}

	/**
	 * Looks for one or two carets in an MDX string, and if present, converts them into a parser position.
	 * 
	 * <p>
	 * Examples:
	 * 
	 * <ul>
	 * <li>findPos("xxx^yyy") yields {"xxxyyy", position 3, line 1 column 4}
	 * <li>findPos("xxxyyy") yields {"xxxyyy", null}
	 * <li>findPos("xxx^yy^y") yields {"xxxyyy", position 3, line 4 column 4 through line 1 column 6}
	 * </ul>
	 * 
	 * @param code
	 *            Source code
	 * @return object containing source code annotated with region
	 */
	public static RegionAndSource findPos(String code) {
		int firstCaret = code.indexOf('^');
		if (firstCaret < 0) {
			return new RegionAndSource(code, null);
		}
		int secondCaret = code.indexOf('^', firstCaret + 1);
		if (secondCaret < 0) {
			String codeSansCaret = code.substring(0, firstCaret) + code.substring(firstCaret + 1);
			int[] start = indexToLineCol(code, firstCaret);
			return new RegionAndSource(codeSansCaret, new ParseRegion(start[0], start[1]));
		} else {
			String codeSansCaret = code.substring(0, firstCaret) + code.substring(firstCaret + 1, secondCaret)
					+ code.substring(secondCaret + 1);
			int[] start = indexToLineCol(code, firstCaret);

			// subtract 1 because first caret pushed the string out
			--secondCaret;

			// subtract 1 because the col position needs to be inclusive
			--secondCaret;
			int[] end = indexToLineCol(code, secondCaret);
			return new RegionAndSource(codeSansCaret, new ParseRegion(start[0], start[1], end[0], end[1]));
		}
	}

	/**
	 * Returns the (1-based) line and column corresponding to a particular (0-based) offset in a string.
	 * 
	 * <p>
	 * Converse of {@link #lineColToIndex(String, int, int)}.
	 * 
	 * @param code
	 *            Source code
	 * @param i
	 *            Offset within source code
	 * @return 2-element array containing line and column
	 */
	private static int[] indexToLineCol(String code, int i) {
		int line = 0;
		int j = 0;
		while (true) {
			String s;
			int rn = code.indexOf("\r\n", j);
			int r = code.indexOf("\r", j);
			int n = code.indexOf("\n", j);
			int prevj = j;
			if ((r < 0) && (n < 0)) {
				assert rn < 0;
				s = null;
				j = -1;
			} else if ((rn >= 0) && (rn < n) && (rn <= r)) {
				s = "\r\n";
				j = rn;
			} else if ((r >= 0) && (r < n)) {
				s = "\r";
				j = r;
			} else {
				s = "\n";
				j = n;
			}
			if ((j < 0) || (j > i)) {
				return new int[] { line + 1, i - prevj + 1 };
			}
			assert s != null;
			j += s.length();
			++line;
		}
	}

	/**
	 * Finds the position (0-based) in a string which corresponds to a given line and column (1-based).
	 * 
	 * <p>
	 * Converse of {@link #indexToLineCol(String, int)}.
	 * 
	 * @param code
	 *            Source code
	 * @param line
	 *            Line number
	 * @param column
	 *            Column number
	 * @return Offset within source code
	 */
	private static int lineColToIndex(String code, int line, int column) {
		--line;
		--column;
		int i = 0;
		while (line-- > 0) {
			// Works on linux where line ending is "\n";
			// also works on windows where line ending is "\r\n".
			// Even works if they supply linux strings on windows.
			i = code.indexOf("\n", i) + "\n".length();
		}
		return i + column;
	}

	/**
	 * Generates a string of the source code annotated with caret symbols ("^") at the beginning and end of the region.
	 * 
	 * <p>
	 * For example, for the region <code>(1, 9, 1, 12)</code> and source <code>"values (foo)"</code>, yields the string
	 * <code>"values (^foo^)"</code>.
	 * 
	 * @param source
	 *            Source code
	 * @return Source code annotated with position
	 */
	public String annotate(String source) {
		return addCarets(source, startLine, startColumn, endLine, endColumn);
	}

	/**
	 * Converts a string to a string with one or two carets in it. For example,
	 * <code>addCarets("values (foo)", 1, 9, 1, 11)</code> yields "values (^foo^)".
	 * 
	 * @param sql
	 *            Source code
	 * @param line
	 *            Line number
	 * @param col
	 *            Column number
	 * @param endLine
	 *            Line number of end of region
	 * @param endCol
	 *            Column number of end of region
	 * @return String annotated with region
	 */
	private static String addCarets(String sql, int line, int col, int endLine, int endCol) {
		String sqlWithCarets;
		int cut = lineColToIndex(sql, line, col);
		sqlWithCarets = sql.substring(0, cut) + "^" + sql.substring(cut);
		if ((col != endCol) || (line != endLine)) {
			cut = lineColToIndex(sqlWithCarets, endLine, endCol + 1);
			++cut; // for caret
			if (cut < sqlWithCarets.length()) {
				sqlWithCarets = sqlWithCarets.substring(0, cut) + "^" + sqlWithCarets.substring(cut);
			} else {
				sqlWithCarets += "^";
			}
		}
		return sqlWithCarets;
	}

	/**
	 * Combination of a region within an MDX statement with the source text of the whole MDX statement.
	 * 
	 * <p>
	 * Useful for reporting errors. For example, the error in the statement
	 * 
	 * <blockquote>
	 * 
	 * <pre>
	 * SELECT {<b><i>[Measures].[Units In Stock]</i></b>} ON COLUMNS
	 * FROM [Sales]
	 * </pre>
	 * 
	 * </blockquote>
	 * 
	 * has source "SELECT {[Measures].[Units In Stock]} ON COLUMNS\nFROM [Sales]" and region [1:9, 1:34].
	 */
	public static class RegionAndSource {
		public final String source;
		public final ParseRegion region;

		/**
		 * Creates a RegionAndSource.
		 * 
		 * @param source
		 *            Source MDX code
		 * @param region
		 *            Coordinates of region within MDX code
		 */
		public RegionAndSource(String source, ParseRegion region) {
			this.source = source;
			this.region = region;
		}
	}
}

// End ParseRegion.java
